/**
 * PHOSIDE: PHosphorylation Site IDentification Engine.
 * Copyright 2009 Digital Biology Lab, University of Missouri.
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 3 of the License, or (at your option) any later
 * version. <p/> This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the License for more
 * details.
 */

package phoside.ui;

import java.awt.Color;
import java.awt.Font;

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTable;
import javax.swing.JTextPane;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.text.SimpleAttributeSet;

import org.apache.commons.lang.StringUtils;

import phoside.PhosideInit;
import phoside.PhosphoProteins;
import phoside.Protein;

import phoside.classify.result.PhosidePredictionResult;
import phoside.classify.result.PhosidePredictionResultImpl;
import phoside.classify.result.ResultInterpreter;
import phoside.classify.result.ResultUtil;

import phoside.data.util.PhosphoDataUtil;

import phoside.io.ProteinsWriter;
import phoside.io.fasta.DefaultFastaProteinsWriter;
import phoside.io.fasta.PhosphoWriterToFastaWithSitesInSequence;
import phoside.io.fasta.ResidueAnnotator;
import phoside.io.xml.PhosideXmlProteinsWriter;
import phoside.io.xml.PhosphoProteinFieldValueFormatter;
import phoside.io.xml.PhosideResultFieldValueFormatter;

import phoside.ui.task.AbstractTask;
import phoside.ui.task.ProteinsWriteTask;
import phoside.ui.task.SerializeTask;
import phoside.ui.task.TaskUtil;

import phoside.ui.util.CMap;
import phoside.ui.util.CTable;
import phoside.ui.util.GradientSliderUI;

import phoside.util.FileExtensionsFilter;
import phoside.util.FilePathParser;
import phoside.util.IOUtil;

/**
 *
 * @author gjj
 */
public class PhosideResultPanel extends javax.swing.JPanel {

    public PhosideResultPanel( String panelName,
            Map<String,PhosidePredictionResult> results) {
        this(panelName, results, false);
    }

    /** Creates new form PhosideResultPanel */
    public PhosideResultPanel( String panelName,
            Map<String,PhosidePredictionResult> results,
            boolean saved) {
        this.panelName = panelName;
        this.results = results;
        this.saved = saved;
        
        initComponents();

        specificity = 0.95;
        setResult(results.keySet().iterator().next());

        resetProteinList();
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {
        java.awt.GridBagConstraints gridBagConstraints;

        resDispPanel = new javax.swing.JPanel();
        javax.swing.JPanel modelPanel = new javax.swing.JPanel();
        modelComboBox = new javax.swing.JComboBox();
        javax.swing.JPanel accPanel = new javax.swing.JPanel();
        accComboBox = new javax.swing.JComboBox();
        javax.swing.JPanel optionPanel = new javax.swing.JPanel();
        phosphoCheckBox = new javax.swing.JCheckBox();
        alphabetCheckBox = new javax.swing.JCheckBox();
        displayTabbedPane = new javax.swing.JTabbedPane();
        sequenceScrollPane = new javax.swing.JScrollPane();
        sitesScrollPane = new javax.swing.JScrollPane();
        javax.swing.JPanel adjustPanel = new javax.swing.JPanel();
        javax.swing.JPanel threholdPanel = new javax.swing.JPanel();
        javax.swing.JPanel sliderBasePanel = new javax.swing.JPanel();
        sliderTitleComboBox = new javax.swing.JComboBox();
        javax.swing.JPanel specificityPanel = new javax.swing.JPanel();
        javax.swing.JLabel specificityLabel = new javax.swing.JLabel();
        specificityTextField = new javax.swing.JTextField();
        javax.swing.JLabel percLabel = new javax.swing.JLabel();
        javax.swing.JLabel scoreCutoffLabel = new javax.swing.JLabel();
        scoreCutoffTextField = new javax.swing.JTextField();
        javax.swing.JPanel numSitesPanel = new javax.swing.JPanel();
        numOfSitesComboBox = new javax.swing.JComboBox();
        specificitySlider = new javax.swing.JSlider();
        javax.swing.JPanel exportPanel = new javax.swing.JPanel();
        exportComboBox = new javax.swing.JComboBox();
        javax.swing.JPanel closePanel = new javax.swing.JPanel();
        javax.swing.JButton saveButton = new javax.swing.JButton();

        setLayout(new java.awt.GridBagLayout());

        resDispPanel.setLayout(new java.awt.GridBagLayout());

        modelPanel.setVisible(results.size()>1);
        modelPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Select model"));
        modelPanel.setLayout(new javax.swing.BoxLayout(modelPanel, javax.swing.BoxLayout.LINE_AXIS));

        modelComboBox.setModel(new javax.swing.DefaultComboBoxModel(results.keySet().toArray(new String[0])));
        modelComboBox.setSelectedIndex(0);
        modelComboBox.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                modelComboBoxActionPerformed(evt);
            }
        });
        modelPanel.add(modelComboBox);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 0;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.weightx = 1.0;
        gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 3);
        resDispPanel.add(modelPanel, gridBagConstraints);

        accPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Select protein(s)"));
        accPanel.setLayout(new java.awt.GridBagLayout());

        Vector pros = new Vector(results.values().iterator().next().proteins());
        pros.add("Display all listed proteins");
        accComboBox.setModel(new javax.swing.DefaultComboBoxModel(pros));
        accComboBox.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                accComboBoxActionPerformed(evt);
            }
        });
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START;
        gridBagConstraints.weightx = 1.0;
        gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 3);
        accPanel.add(accComboBox, gridBagConstraints);

        optionPanel.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.LEFT, 3, 3));

        phosphoCheckBox.setText("List predicted phosphoproteins only");
        phosphoCheckBox.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                phosphoCheckBoxActionPerformed(evt);
            }
        });
        optionPanel.add(phosphoCheckBox);

        alphabetCheckBox.setText("List alphabetically (according to accessions)");
        alphabetCheckBox.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                alphabetCheckBoxActionPerformed(evt);
            }
        });
        optionPanel.add(alphabetCheckBox);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 0;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        accPanel.add(optionPanel, gridBagConstraints);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.weightx = 1.0;
        resDispPanel.add(accPanel, gridBagConstraints);

        displayTextPane = new ViewResultTextPane();
        sequenceScrollPane.setViewportView(displayTextPane);

        displayTabbedPane.addTab("Sequence(s)", sequenceScrollPane);

        displayTable = new ViewResultTable();
        sitesScrollPane.setViewportView(displayTable);

        displayTabbedPane.addTab("Predicted Sites", sitesScrollPane);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 2;
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        gridBagConstraints.weightx = 1.0;
        gridBagConstraints.weighty = 1.0;
        resDispPanel.add(displayTabbedPane, gridBagConstraints);

        adjustPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Stringency setting"));
        adjustPanel.setLayout(new java.awt.GridBagLayout());

        threholdPanel.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.CENTER, 5, 0));

        sliderBasePanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Sliding according to"));
        sliderBasePanel.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.CENTER, 5, 0));

        sliderTitleComboBox.setMinimumSize(new java.awt.Dimension(150, 18));
        sliderTitleComboBox.setPreferredSize(new java.awt.Dimension(150, 22));
        sliderTitleComboBox.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                sliderTitleComboBoxActionPerformed(evt);
            }
        });
        sliderBasePanel.add(sliderTitleComboBox);

        threholdPanel.add(sliderBasePanel);

        specificityPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Cutoff"));
        specificityPanel.setLayout(new java.awt.GridBagLayout());

        specificityLabel.setText("Specificity:");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridy = 0;
        gridBagConstraints.insets = new java.awt.Insets(3, 3, 3, 0);
        specificityPanel.add(specificityLabel, gridBagConstraints);

        specificityTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
        specificityTextField.setMinimumSize(new java.awt.Dimension(60, 19));
        specificityTextField.setPreferredSize(new java.awt.Dimension(60, 19));
        specificityTextField.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                specificityTextFieldActionPerformed(evt);
            }
        });
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridy = 0;
        gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 1);
        specificityPanel.add(specificityTextField, gridBagConstraints);

        percLabel.setText("%");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridy = 0;
        gridBagConstraints.insets = new java.awt.Insets(0, 1, 0, 20);
        specificityPanel.add(percLabel, gridBagConstraints);

        scoreCutoffLabel.setText("Score:");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridy = 0;
        specificityPanel.add(scoreCutoffLabel, gridBagConstraints);

        scoreCutoffTextField.setHorizontalAlignment(javax.swing.JTextField.RIGHT);
        scoreCutoffTextField.setMinimumSize(new java.awt.Dimension(60, 19));
        scoreCutoffTextField.setPreferredSize(new java.awt.Dimension(60, 19));
        scoreCutoffTextField.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                scoreCutoffTextFieldActionPerformed(evt);
            }
        });
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridy = 0;
        gridBagConstraints.insets = new java.awt.Insets(0, 3, 0, 20);
        specificityPanel.add(scoreCutoffTextField, gridBagConstraints);

        threholdPanel.add(specificityPanel);

        numSitesPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("No. of predicted sites in all proteins"));
        numSitesPanel.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.CENTER, 5, 0));

        numOfSitesComboBox.setEditable(true);
        numOfSitesComboBox.setPreferredSize(new java.awt.Dimension(180, 22));
        numOfSitesComboBox.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                numOfSitesComboBoxActionPerformed(evt);
            }
        });
        numSitesPanel.add(numOfSitesComboBox);

        threholdPanel.add(numSitesPanel);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        adjustPanel.add(threholdPanel, gridBagConstraints);

        specificitySlider.setPaintLabels(true);
        specificitySlider.addChangeListener(new javax.swing.event.ChangeListener() {
            public void stateChanged(javax.swing.event.ChangeEvent evt) {
                specificitySliderStateChanged(evt);
            }
        });
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 2;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.weightx = 1.0;
        adjustPanel.add(specificitySlider, gridBagConstraints);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 3;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.weightx = 1.0;
        gridBagConstraints.insets = new java.awt.Insets(2, 2, 2, 2);
        resDispPanel.add(adjustPanel, gridBagConstraints);

        exportPanel.setLayout(new javax.swing.BoxLayout(exportPanel, javax.swing.BoxLayout.LINE_AXIS));

        exportComboBox.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "Export this result as...", "Tab-delimited text file", "FASTA file", "PHOSIDE XML file" }));
        exportComboBox.setMinimumSize(new java.awt.Dimension(200, 18));
        exportComboBox.setPreferredSize(new java.awt.Dimension(200, 22));
        exportComboBox.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exportComboBoxActionPerformed(evt);
            }
        });
        exportPanel.add(exportComboBox);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 4;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START;
        gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5);
        resDispPanel.add(exportPanel, gridBagConstraints);

        closePanel.setLayout(new javax.swing.BoxLayout(closePanel, javax.swing.BoxLayout.LINE_AXIS));

        saveButton.setText("Save As...");
        saveButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                saveButtonActionPerformed(evt);
            }
        });
        closePanel.add(saveButton);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 4;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_END;
        gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5);
        resDispPanel.add(closePanel, gridBagConstraints);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 0;
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        gridBagConstraints.weightx = 1.0;
        gridBagConstraints.weighty = 1.0;
        add(resDispPanel, gridBagConstraints);
    }// </editor-fold>//GEN-END:initComponents

    private void specificityTextFieldActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_specificityTextFieldActionPerformed
        double value;
        try {
             value = Double.valueOf(specificityTextField.getText());
        } catch (Exception e) {
            JOptionPane.showMessageDialog(this, "Please specify a number between 0 and 100.");
            return;
        }

        if (value<0 || value>100) {
            JOptionPane.showMessageDialog(this, "Please specify a number between 0 and 100.");
            return;
        }

        if (value/100==specificity)
            return;

        this.specificity = value/100;
        this.threshold = result.getResultInterpreter().getThreshold(1-value/100, false);
        scoreCutoffTextField.setText(String.format("%.2f", threshold));

        lockScore = true;
        lockSpec = true;
        
        specificitySlider.setValue((int)Math.round(maxTick*(threshold-minScore)/(maxScore-minScore)));
        //sensitivitySlider.setValue(sensitivity);
        //sensitivityTextField.setText(""+sensitivity);
//        if (phosphoCheckBox.isSelected()) {
//            resetProteinList();
//        } else {
//            resetDisplay();
//        }

        lockScore = false;
        lockSpec = false;
    }//GEN-LAST:event_specificityTextFieldActionPerformed

    private void specificitySliderStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_specificitySliderStateChanged
        if (ignoreSlideChangeEvent)
            return;

        javax.swing.JSlider source = (javax.swing.JSlider)evt.getSource();

        if (!lockScore) {
            int value = (int)source.getValue();
            threshold = (maxScore-minScore)*value/maxTick+minScore;
            scoreCutoffTextField.setText(String.format("%.2f", threshold));
        }

        if (!lockSpec) {
            if (result.getResultInterpreter()!=null) {
                setSpecificity(threshold);
                specificityTextField.setText(String.format("%.2f", specificity*100));
            }
        }

        if (!lockNumSites) {
            setNumberOfSites(threshold);
            boolean oldLockScore = lockScore;
            lockScore = true;
            numEditor.setNum(numOfSites);
            lockScore = oldLockScore;
            resetSelectedNumSites();
        }
        
        if (!source.getValueIsAdjusting()) { // does not work
            result.setThreshold(threshold);
            if (phosphoCheckBox.isSelected()) {
                resetProteinList();
            } else {
                resetDisplay();
            }
        }
    }//GEN-LAST:event_specificitySliderStateChanged

    private void accComboBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_accComboBoxActionPerformed
        resetDisplay();
    }//GEN-LAST:event_accComboBoxActionPerformed

    private void exportComboBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportComboBoxActionPerformed
        String name = panelName.replaceAll("[\\\\/:\\*\\?\"<>\\|]+", "-");
        String defaultFile = PhosideInit.defaultPath+File.separator+name;
        if (sliderTitleComboBox.getSelectedItem().equals(SPECIFICITY)) {
            defaultFile += String.format("-Sp_%.2f", specificity);
        } else {
            defaultFile += String.format("-Score_%.3f", threshold);
        }
        defaultFile += ".result";
        
        switch (exportComboBox.getSelectedIndex()) {
            case 1: // tab-delimited text file
            {
                JFileChooser fc = new JFileChooser(PhosideInit.defaultPath);
                fc.setSelectedFile(new File(defaultFile));

                ArrayList<String> exts = new ArrayList<String>(1);
                String fasta = "txt";
                exts.add(fasta);
                FileExtensionsFilter fastaFilter = new FileExtensionsFilter(exts,"Tab-delimited file (.txt)");
                fc.addChoosableFileFilter(fastaFilter);

                //fc.setAcceptAllFileFilterUsed(true);
                fc.setDialogTitle("Save the the result to...");
                int returnVal = fc.showSaveDialog(this);
                if (returnVal == JFileChooser.APPROVE_OPTION) {
                    File file = fc.getSelectedFile();
                    PhosideInit.defaultPath = file.getParent();

                    String filePath = PhosideInit.defaultPath + File.separator + file.getName();

                    String ext = FilePathParser.getExt(filePath);
                    if (ext==null||!ext.equalsIgnoreCase("txt")) {
                        filePath += ".txt";
                    }

                    if (IOUtil.fileExist(filePath)) {
                        int ret = JOptionPane.showConfirmDialog(this, "Are you sure to replace the existing file?", "Relace the existing file?", JOptionPane.YES_NO_OPTION);
                        if (ret==JOptionPane.NO_OPTION)
                            break;
                    }
                                        
                    Vector<Vector> data = formatData(resultDisplay,false,true);
                    int n = data.size();
                    ArrayList<String> dataOut = new ArrayList(n+1);
                    dataOut.add(StringUtils.join(header.iterator(), '\t'));
                    
                    for (Vector vec : data) {
                        dataOut.add(StringUtils.join(vec.iterator(), '\t'));
                    }

                    try {
                        IOUtil.writeCollectionAscii(dataOut, filePath);
                    } catch (IOException e) {
                        e.printStackTrace();
                        JOptionPane.showMessageDialog(this, "Error: failed to write the file.");
                        break;
                    }

                    JOptionPane.showMessageDialog(this, "Successfully exported.");
                }

                break;
            }
            case 2: // fasta
            {
                JFileChooser fc = new JFileChooser(PhosideInit.defaultPath);
                fc.setSelectedFile(new File(defaultFile));

                ArrayList<String> exts = new ArrayList<String>(1);
                String fasta = "fasta";
                exts.add(fasta);
                FileExtensionsFilter fastaFilter = new FileExtensionsFilter(exts,"Fasta file (.fasta)");
                fc.addChoosableFileFilter(fastaFilter);

                //fc.setAcceptAllFileFilterUsed(true);
                fc.setDialogTitle("Save the the result to...");
                int returnVal = fc.showSaveDialog(this);
                if (returnVal == JFileChooser.APPROVE_OPTION) {
                    File file = fc.getSelectedFile();
                    PhosideInit.defaultPath = file.getParent();

                    String filePath = PhosideInit.defaultPath + File.separator + file.getName();

                    String ext = FilePathParser.getExt(filePath);
                    if (ext==null||!ext.equalsIgnoreCase("fasta")) {
                        filePath += ".fasta";
                    }
                    
                    if (IOUtil.fileExist(filePath)) {
                        int ret = JOptionPane.showConfirmDialog(this, "Are you sure to replace the existing file?", "Relace the existing file?", JOptionPane.YES_NO_OPTION);
                        if (ret==JOptionPane.NO_OPTION)
                            break;
                    }

                    ProteinsWriter writer;
                    try {
                        writer = new PhosphoWriterToFastaWithSitesInSequence(filePath);
                    } catch (IOException e) {
                        JOptionPane.showMessageDialog(this, "Error: Failed to export.");
                        e.printStackTrace();
                        return;
                    }

                    ProteinsWriteTask writeTask = new ProteinsWriteTask(resultDisplay, writer);
                    TaskUtil.execute(writeTask);
                    if (!writeTask.success()) {
                        JOptionPane.showMessageDialog(this, "Failed to export.");
                        break;
                    }

                    JOptionPane.showMessageDialog(this, "Successfully exported.");
                }

                break;
            }
            case 3: // xml
            {
                JFileChooser fc = new JFileChooser(PhosideInit.defaultPath);
                fc.setSelectedFile(new File(defaultFile));

                ArrayList<String> exts = new ArrayList<String>(1);
                String xml = "xml";
                exts.add(xml);
                FileExtensionsFilter xmlFilter = new FileExtensionsFilter(exts,"XML file (.xml)");
                fc.addChoosableFileFilter(xmlFilter);

                //fc.setAcceptAllFileFilterUsed(true);
                fc.setDialogTitle("Save the the result to...");
                int returnVal = fc.showSaveDialog(this);
                if (returnVal == JFileChooser.APPROVE_OPTION) {
                    File file = fc.getSelectedFile();
                    PhosideInit.defaultPath = file.getParent();

                    String filePath = PhosideInit.defaultPath + File.separator + file.getName();

                    String ext = FilePathParser.getExt(filePath);
                    if (ext==null||!ext.equalsIgnoreCase("xml")) {
                        filePath += ".xml";
                    }

                    if (IOUtil.fileExist(filePath)) {
                        int ret = JOptionPane.showConfirmDialog(this, "Are you sure to replace the existing file?", "Relace the existing file?", JOptionPane.YES_NO_OPTION);
                        if (ret==JOptionPane.NO_OPTION)
                            break;
                    }

                    ProteinsWriter writer = new PhosideXmlProteinsWriter(filePath,
                                new PhosphoProteinFieldValueFormatter() {
                        public String write(String fieldName, Object fieldValue) {
                            if (fieldName.compareTo(phoside.util.StaticFinalValues.PREDICTION)==0)
                                return null;
                            return super.write(fieldName, fieldValue);
                        }
                    });

                    ProteinsWriteTask xmlWriteTask = new ProteinsWriteTask(resultDisplay, writer);
                    TaskUtil.execute(xmlWriteTask);
                    if (!xmlWriteTask.success()) {
                        JOptionPane.showMessageDialog(this, "Failed to export.");
                        break;
                    }

                    JOptionPane.showMessageDialog(this, "Successfully exported.");
                }
                
                break;
            }
        }
        exportComboBox.setSelectedIndex(0);
    }//GEN-LAST:event_exportComboBoxActionPerformed

    private void saveButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveButtonActionPerformed
        saveResult(true);
    }//GEN-LAST:event_saveButtonActionPerformed

    private void phosphoCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_phosphoCheckBoxActionPerformed
        resetProteinList();
    }//GEN-LAST:event_phosphoCheckBoxActionPerformed

    private void alphabetCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_alphabetCheckBoxActionPerformed
        resetProteinList();
    }//GEN-LAST:event_alphabetCheckBoxActionPerformed

    private void sliderTitleComboBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_sliderTitleComboBoxActionPerformed
        setSliderLabels();
    }//GEN-LAST:event_sliderTitleComboBoxActionPerformed

    private void scoreCutoffTextFieldActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_scoreCutoffTextFieldActionPerformed
        double value;
        try {
             value = Double.valueOf(scoreCutoffTextField.getText());
        } catch (Exception e) {
            e.printStackTrace();
            JOptionPane.showMessageDialog(this, "Please input a number.");
            return;
        }

        lockScore = true;

        this.threshold = value;
        specificitySlider.setValue((int)Math.round(maxTick*(threshold-minScore)/(maxScore-minScore)));
//        if (phosphoCheckBox.isSelected()) {
//            resetProteinList();
//        } else {
//            resetDisplay();
//        }

        lockScore = false;
    }//GEN-LAST:event_scoreCutoffTextFieldActionPerformed

    private void numOfSitesComboBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_numOfSitesComboBoxActionPerformed
        if (evt.getActionCommand().compareTo("comboBoxChanged")!=0)
            return;

        if (lockScore||lockSpec) //
            return;

        numberOfSitesChanged();
    }//GEN-LAST:event_numOfSitesComboBoxActionPerformed

    private void modelComboBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_modelComboBoxActionPerformed
        String model = (String)modelComboBox.getSelectedItem();
        setResult(model);
        resetProteinList();
    }//GEN-LAST:event_modelComboBoxActionPerformed

    private void numberOfSitesChanged() {
        int num;
        try {
            num = numEditor.getNum();//numOfSitesComboBox.getSelectedIndex();
        } catch (Exception e) {
            //e.printStackTrace();
            num = -1;
        }

        if (num==numOfSites)
            return;

        List<Double> allScores = result.descendPredictionScores();
        if (num<0 || num>allScores.size()) {
            JOptionPane.showMessageDialog(this, "Please choose a integer number between 0 and "+allScores.size());
            return;
        }

        numOfSites = num;

        if (numOfSites==0) {
            this.threshold = maxScore + (maxScore-minScore)/maxTick;
        } else {
            this.threshold = allScores.get(numOfSites-1);
        }
        scoreCutoffTextField.setText(String.format("%.2f", threshold));

        lockScore = true;
        lockNumSites = true;

        specificitySlider.setValue((int)Math.round(maxTick*(threshold-minScore)/(maxScore-minScore)));

        lockScore = false;
        lockNumSites = false;
    }

    private void setResult(String model) {
        result = results.get(model);

        List<Double> allScores = result.descendPredictionScores();
        maxScore = allScores.get(0);
        minScore = allScores.get(allScores.size()-1);

        if (result.getResultInterpreter()!=null) {
            double fpr0 = result.getResultInterpreter().getThreshold(0.0, false);
            double fpr1 = result.getResultInterpreter().getThreshold(1.0, false);
            if (maxScore<fpr0) {
                maxScore = fpr0;
            }
            if (minScore>fpr1) {
                minScore = fpr1;
            }

            threshold = result.getThrehold();//.getResultInterpreter().getThreshold(0.05, false);
            
            if (Double.isInfinite(threshold)) {
                //specificity = 0.95;
                threshold = result.getResultInterpreter().getThreshold(0.05, false);
            } else {
                specificity = 1-result.getResultInterpreter().getFPR(threshold);
            }
            
        } else {
            this.threshold = (maxScore+minScore)/2;
        }

        setNumberOfSites(threshold);
        result.setThreshold(threshold);

        double eps = (maxScore-minScore)/maxTick; // one more tic each side
        maxScore += eps;
        minScore -= eps;

        // set ui
        ignoreSlideChangeEvent = true;

        setSliderTitleModel();

        String textSpec;
        if (result.getResultInterpreter()==null) {
            textSpec = "N/A";
            specificityTextField.setEditable(false);
        } else {
            textSpec = String.format("%.2f", specificity*100);
        }
        specificityTextField.setText(textSpec);

        scoreCutoffTextField.setText(String.format("%.2f", threshold));
        
        setNumberOfSitesComboBoxModel();

        resetSelectedNumSites();

        setSliderLabels();
        setMapScoreColor();

        specificitySlider.setValue((int)Math.round(maxTick*(threshold-minScore)/(maxScore-minScore)));

        ignoreSlideChangeEvent = false;
    }

    public void setGroundTruth(PhosphoProteins groundTruth) {
        boolean succ;
        try {
            succ = ResultUtil.resetResultInterpreter(result, groundTruth);
        } catch (Exception e) {
            e.printStackTrace();
            succ = false;
        }
        if (!succ) {
            JOptionPane.showMessageDialog(this, "Failed to integrate the ground truth data.");
            return;
        }

        specificityTextField.setEditable(true);

        //setThreshold(threshold);
        setSpecificity(threshold);
        specificityTextField.setText(String.format("%.2f", specificity*100));

        sliderTitleComboBox.setModel(new javax.swing.DefaultComboBoxModel(
                new String[] {SPECIFICITY, SCORE}));

        setSliderLabels();
        setMapScoreColor();

        this.groundTruth = groundTruth;
        resetDisplay();

        JOptionPane.showMessageDialog(this, "Done.");
    }

    private static final int maxTick = 10000;
    private static final String SPECIFICITY = "Specificity";
    private static final String SCORE = "Score";
    
    private void setSliderTitleModel() {
        String[] ops;
        if (result.getResultInterpreter()==null) {
            ops = new String[] {SCORE};
        } else {
            ops = new String[] {SPECIFICITY,SCORE};
        }
        sliderTitleComboBox.setModel(new javax.swing.DefaultComboBoxModel(ops));
    }

    private void setSliderLabels() {
        specificitySlider.setMaximum(maxTick);
        specificitySlider.setMinorTickSpacing(maxTick/20);

        Hashtable labelTable = new Hashtable();

        if (sliderTitleComboBox.getSelectedItem().equals(SPECIFICITY)) {
            ResultInterpreter ri = result.getResultInterpreter();
            double scoreAtSpec0 = ri.getThreshold(1.0, false);
            double scoreAtSpec50 = ri.getThreshold(0.5, false);
            double scoreAtSpec80 = ri.getThreshold(0.2, false);
            double scoreAtSpec90 = ri.getThreshold(0.1, false);
            double scoreAtSpec95 = ri.getThreshold(0.05, false);
            double scoreAtSpec100 = ri.getThreshold(0.0, false);

            //Create the label table
            labelTable.put((int)(maxTick*(scoreAtSpec0-minScore)/(maxScore-minScore)), new JLabel("0"));
            labelTable.put((int)(maxTick*(scoreAtSpec50-minScore)/(maxScore-minScore)), new JLabel("50"));
            labelTable.put((int)(maxTick*(scoreAtSpec80-minScore)/(maxScore-minScore)), new JLabel("80"));
            labelTable.put((int)(maxTick*(scoreAtSpec90-minScore)/(maxScore-minScore)), new JLabel("90"));
            labelTable.put((int)(maxTick*(scoreAtSpec95-minScore)/(maxScore-minScore)), new JLabel("95"));
            labelTable.put((int)(maxTick*(scoreAtSpec100-minScore)/(maxScore-minScore)), new JLabel("100%"));

            
        }  else {
            double step = Math.round(2*(Math.floor(maxScore)-Math.ceil(minScore))/10)/4;
            if (step==0)
                step = 0.5;

            double score = Math.ceil(2*minScore)/2;
            while (score <= maxScore) {
                labelTable.put((int)(maxTick*(score-minScore)/(maxScore-minScore)), new JLabel(String.format("%.1f", score)));
                score += step;
            }
        }

        specificitySlider.setLabelTable(labelTable);
    }

    private void setMapScoreColor() {
        ResultInterpreter ri = result.getResultInterpreter();
        mapScoreColor = new TreeMap();
        if (ri!=null) {
            double scoreAtSpec0 = ri.getThreshold(1.0, false);
            double scoreAtSpec50 = ri.getThreshold(0.5, false);
            double scoreAtSpec80 = ri.getThreshold(0.2, false);
            double scoreAtSpec90 = ri.getThreshold(0.1, false);
            double scoreAtSpec95 = ri.getThreshold(0.05, false);
            double scoreAtSpec100 = ri.getThreshold(0.0, false);
            mapScoreColor.put(scoreAtSpec0, new Color(255,255,0));
            mapScoreColor.put(scoreAtSpec50, new Color(0,255,0));
            mapScoreColor.put(scoreAtSpec80, new Color(0,255,200));
            mapScoreColor.put(scoreAtSpec90, new Color(50,100,200));
            mapScoreColor.put(scoreAtSpec95, new Color(150,50,200));
            mapScoreColor.put(scoreAtSpec100, new Color(250,0,0));
            if (scoreAtSpec0>minScore) {
                mapScoreColor.put(minScore, new Color(255,255,0));
            }
            if (scoreAtSpec100<maxScore) {
                mapScoreColor.put(maxScore, new Color(250,0,0));
            }
        } else {
            mapScoreColor.put(minScore, Color.BLUE);
            mapScoreColor.put(maxScore, Color.RED);
        }

        specificitySlider.setUI(new GradientSliderUI(specificitySlider,mapScoreColor,minScore,maxScore));
    }

    private void setNumberOfSitesComboBoxModel() {
        List<Double> allScores = result.descendPredictionScores();
        int n = allScores.size();
        int digit = (int)Math.floor(Math.log10(n));

        Vector<Integer> options = new Vector();
       options.add(0);
        for (int d=0; d<=digit; d++) {
            for (int i=1; i<=9; i++) {
                int num = i*(int)Math.pow(10, d);
                if (num>=n)
                    break;
                options.add(num);
            }
        }

        options.add(n);

        numOfSitesComboBox.setModel(new javax.swing.DefaultComboBoxModel(options));
        //numOfSitesComboBox.setSelectedIndex(numOfSites);

        numEditor = new NumSitesComboBoxEditor();
        numOfSitesComboBox.setEditor(numEditor);
        numEditor.setNum(numOfSites);
        numEditor.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                numberOfSitesChanged();
                resetSelectedNumSites();
            }
        });
    }

    private void resetSelectedNumSites() {
        boolean oldLockScore = lockScore;
        lockScore = true;
        lockNumSites = true;
        if (numOfSites==0)
            numOfSitesComboBox.setSelectedItem(numOfSites);
        else {
            int scale = (int)Math.pow(10, Math.floor(Math.log10(numOfSites)));
            numOfSitesComboBox.setSelectedItem(numOfSites/scale * scale);
        }
        lockNumSites = false;
        lockScore = oldLockScore;
    }

    private void resetProteinList() {
        boolean phospho = phosphoCheckBox.isSelected();
        boolean alphabetical = alphabetCheckBox.isSelected();

        if (!phospho && !alphabetical) {
            resultDisplay = result;
        } else {
            resultDisplay = result.copyTo(null, phospho, alphabetical);
        }

        Vector vec = new Vector(resultDisplay.proteinCount()+1);
        vec.addAll(resultDisplay.proteins());
        vec.add("Display all listed proteins");

        lockDisplay = true;
        Object obj = accComboBox.getSelectedItem();
        accComboBox.setModel(new javax.swing.DefaultComboBoxModel(vec));
        if (obj!=null)
            accComboBox.setSelectedItem(obj);
        else
            accComboBox.setSelectedIndex(0);

        lockDisplay = false;
        resetDisplay(); // this will be called after accComboBox.selectedItem
    }

    public boolean saveResult(boolean showConfirm) {
        JFileChooser fc = new JFileChooser(PhosideInit.defaultPath);
        fc.setSelectedFile(new File(PhosideInit.defaultPath+File.separator+panelName));

        ArrayList<String>  exts = new ArrayList<String>(1);
        String xml = "xml";
        exts.add(xml);
        FileExtensionsFilter xmlFilter = new FileExtensionsFilter(exts,"Xml file without ROC info (.xml)");
        fc.addChoosableFileFilter(xmlFilter);

        exts = new ArrayList<String>(1);
        String pred = "pred";
        exts.add(pred);
        FileExtensionsFilter predFilter = new FileExtensionsFilter(exts,"PHOSIDE Prediction file (.pred)");
        fc.addChoosableFileFilter(predFilter);

        //fc.setAcceptAllFileFilterUsed(true);
        fc.setDialogTitle("Save the the result to...");
        int returnVal = fc.showSaveDialog(this);
        if (returnVal == JFileChooser.APPROVE_OPTION) {
            File file = fc.getSelectedFile();
            PhosideInit.defaultPath = file.getParent();

            String filePath = PhosideInit.defaultPath + File.separator + file.getName();

            if (fc.getFileFilter()==xmlFilter) {
                if (!filePath.toLowerCase().endsWith(xml)) {
                    filePath += "."+xml;
                }
            } else {
                if (!filePath.toLowerCase().endsWith(pred)) {
                    filePath += "."+pred;
                }
            }

            if (IOUtil.fileExist(filePath)) {
                int ret = JOptionPane.showConfirmDialog(this, "Are you sure to replace the existing file?", "Relace the existing file?", JOptionPane.YES_NO_OPTION);
                if (ret==JOptionPane.NO_OPTION)
                    return false;
            }

            AbstractTask task;
            if (fc.getFileFilter()==xmlFilter || filePath.toLowerCase().endsWith(xml)) {
                ProteinsWriter writer = new PhosideXmlProteinsWriter(filePath,
                            new PhosideResultFieldValueFormatter() {
                    public String write(String fieldName, Object fieldValue) {
                        if (fieldName.compareTo(phoside.util.StaticFinalValues.SITES)==0)
                            return null;
                        if (fieldName.compareTo(phoside.util.StaticFinalValues.SITENOTES)==0)
                            return null;
                        return super.write(fieldName, fieldValue);
                    }
                });

                task = new ProteinsWriteTask(resultDisplay, writer);
            } else {
                task = new SerializeTask(result, filePath, "gz");
            }

            TaskUtil.execute(task);
            if (task.success()) {
                if (showConfirm)
                    JOptionPane.showMessageDialog(this, "Result saved!");
                saved = true;
                return true;
            } else {
                JOptionPane.showMessageDialog(this, "Failed to save the result.");
                return false;
            }
        } else {
            return false;
        }
    }

    public void dispose() {
        // TODO: is this enough for garbage collection
        results = null;
        result = null;
        resultDisplay = null;
    }

    public String getPanelName() {
        return panelName;
    }

    public void setPanelName(String panelName) {
        this.panelName = panelName;
    }

//    private void setThreshold(double threshold) {
//        this.threshold = threshold;
//        result.setThreshold(threshold);
//        ResultInterpreter ri = result.getResultInterpreter();
//        if (ri!=null) {
//            specificity = 1-ri.getFPR(threshold);
//        }
//
//        resetNumberOfSites();
//    }

    private void setSpecificity(double threshold) {
        ResultInterpreter ri = result.getResultInterpreter();
        if (ri!=null) {
            specificity = 1-ri.getFPR(threshold);
        }
    }

    private void setNumberOfSites(double threshold) {
        java.util.Comparator comp = new java.util.Comparator() {
            public int	compare(Object o1, Object o2) {
                return -((Double)o1).compareTo((Double)o2);
            }
        };

        List<Double> allScores = result.descendPredictionScores();
        int idx = java.util.Collections.binarySearch(allScores, threshold, comp);
        if (idx<0) {
            numOfSites = -idx-1;
        } else {
            numOfSites = idx+1;
        }
    }

    private void resetDisplay() {
        if (lockDisplay)
            return;
        
        final PhosidePredictionResult proteins;
        int n = accComboBox.getItemCount();
        int idx = accComboBox.getSelectedIndex();
        if (idx==n-1) {
            proteins = resultDisplay;
        } else {
            Protein protein = (Protein)accComboBox.getItemAt(idx);
            proteins = new PhosidePredictionResultImpl(result.getResultInterpreter());
            proteins.addProtein(protein);
        }
/*
        displayTable.setResult(proteins);
        displayTextPane.setResult(proteins);
/*/
        AbstractTask task = new AbstractTask("Preparing data for displaying") {
            public void run() {
                try {
                        taskMonitor.setPercentCompleted(-1);
                        taskMonitor.setStatus("Preparing...");
                        displayTable.setResult(proteins);
                        displayTextPane.setResult(proteins);
                        taskMonitor.setPercentCompleted(100);
                        taskMonitor.setStatus("Done.");
                        this.success = true;
                } catch (Exception e) {
                        taskMonitor.setPercentCompleted(100);
                        taskMonitor.setStatus("Failed.\n"+e.getMessage());
                        e.printStackTrace();
                        return;
                }
            }
        };
        TaskUtil.execute(task);
        if (!task.success()) {
            JOptionPane.showMessageDialog(this, "Failed to preparing the data");
            return;
        }
//*/
    }
    
    private static Vector header;
    private static final String headerPosition = "Position";
    private static final String headerAA = "Amino Acid";
    private static final String headerSeq = "Surr. Sequence";
    private static final String headerScore = "Score";
    private static final String headerSpecificity = "Specificity";

    static {
        header = new Vector();
        header.add(headerPosition);
        header.add(headerAA);
        header.add(headerSeq);
        header.add(headerScore);
        header.add(headerSpecificity);
    }

    private Vector formatData(PhosidePredictionResult result, boolean displayScoreInSequence,
            boolean displayHeader) {
        if (result==null) {
            return null;
        }

        Vector data = new Vector();

        Iterator<Protein> it = result.proteinIterator();
        while (it.hasNext()) {
            Protein protein = it.next();
            String acc = protein.getAccession();

            List<Double> preds = result.getPredictions(acc);
            Set<Integer> sites = result.getSites(acc);
            if (sites==null || sites.isEmpty())
                continue;

            if (displayHeader) {
                Vector vec = new Vector(1);
                vec.add('>'+protein.toString());
                data.add(vec);
            }

            String proteinSeq = protein.getSequence();
            int len = proteinSeq.length();

            for (int site : sites) {
                char aa = proteinSeq.charAt(site);

                double score = preds.get(site);

                String seq;
                if (site<offset) {
                    seq = StringUtils.repeat("*", offset-site)+
                            proteinSeq.substring(0,site);
                } else {
                    seq = proteinSeq.substring(site-offset,site);
                }

                seq += "[";
                if (displayScoreInSequence)
                    seq += score;
                else
                    seq += "p";

                seq += aa+"]";

                int end = site+offset+1;
                if (end>len) {
                    seq += proteinSeq.substring(site+1,len)+
                            StringUtils.repeat("*", end-len);
                } else {
                    seq += proteinSeq.substring(site+1,end);
                }

                Vector vec = new Vector(header.size());
                vec.add(site+1);
                vec.add(aa);
                vec.add(seq);

                vec.add(String.format("%.2f", score));

                String strSpec;
                if (result.getResultInterpreter()!=null) {
                    double spec = 1-result.getResultInterpreter().getFPR(score);
                    strSpec = String.format("%.2f%%", 100*spec);
                } else {
                    strSpec = "";
                }
                vec.add(strSpec);
//                vec.add(String.format("%.2f%%", 100*sens));
                data.add(vec);
            }
        }

        return data;
    }

    public boolean isSaved() {
        return saved;
    }

    private String panelName;
    private Map<String,PhosidePredictionResult> results;
    private PhosidePredictionResult result;
    private PhosidePredictionResult resultDisplay;
    private int numOfSites;
    private PhosphoProteins groundTruth = null;
    private double specificity;
    private double threshold;
    private double maxScore;
    private double minScore;

    private boolean saved;

    private final int offset = 12;

    private ViewResultTextPane displayTextPane;
    private ViewResultTable displayTable;

    private TreeMap<Double,Color> mapScoreColor;

    private boolean lockSpec=false, lockScore=false, lockNumSites=false,
            lockDisplay=false, ignoreSlideChangeEvent = false;

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JComboBox accComboBox;
    private javax.swing.JCheckBox alphabetCheckBox;
    private javax.swing.JTabbedPane displayTabbedPane;
    private javax.swing.JComboBox exportComboBox;
    private javax.swing.JComboBox modelComboBox;
    private javax.swing.JComboBox numOfSitesComboBox;
    private NumSitesComboBoxEditor numEditor;
    private javax.swing.JCheckBox phosphoCheckBox;
    private javax.swing.JPanel resDispPanel;
    private javax.swing.JTextField scoreCutoffTextField;
    private javax.swing.JScrollPane sequenceScrollPane;
    private javax.swing.JScrollPane sitesScrollPane;
    private javax.swing.JComboBox sliderTitleComboBox;
    private javax.swing.JSlider specificitySlider;
    private javax.swing.JTextField specificityTextField;
    // End of variables declaration//GEN-END:variables

    private Color getColorOfSite(double score) {
        double leftScore = 0;
        double rightScore = 0;

        for (double tick : mapScoreColor.keySet()) {
            if (Math.abs(tick-score)<(maxScore-minScore)/maxTick/2)
                return mapScoreColor.get(tick);

            if (tick<score) {
                leftScore = tick;
            } else {
                rightScore = tick;
                break;
            }
        }

        Color leftColor = mapScoreColor.get(leftScore);
        Color rightColor = mapScoreColor.get(rightScore);

        float[] leftRGB = leftColor.getRGBColorComponents(null);
        float[] rightRGB = rightColor.getRGBColorComponents(null);

        float perc = (float)((score-leftScore)/(rightScore-leftScore));

        float red = (rightRGB[0]-leftRGB[0])*perc+leftRGB[0];
        float green = (rightRGB[1]-leftRGB[1])*perc+leftRGB[1];
        float blue = (rightRGB[2]-leftRGB[2])*perc+leftRGB[2];

        return new Color(red, green, blue);
    }

    private Color invertColor(Color color) {
        float[] rgb = color.getRGBColorComponents(null);
        return new Color(1-rgb[0],1-rgb[1],1-rgb[2]);
    }

    class ViewResultTable extends CTable {
        public ViewResultTable() {
            setEnabled(false);
            this.setRowHeight(20);
            setHeaderRowMerged();
        }

        private void setHeaderRowMerged() {
            final int columns = header.size();
            CMap map = new CMap() {
                public int span(int row, int column)  {
                    if (isHeaderRow(row)) return columns;
                    return 1;
                }
                public int visibleCell(int row, int column)  {
                    if (isHeaderRow(row)) return 0;
                    return column;
                }
            };

            setCMap(map);
        }

        protected void setCellRenderer() {
            final TableCellRenderer headAlignRender = new TableCellRenderer() {
                private DefaultTableCellRenderer  cellRenderer = new DefaultTableCellRenderer();
                public java.awt.Component getTableCellRendererComponent(JTable table, Object value,
                        boolean isSelected, boolean hasFocus, int row, int column) {
                    java.awt.Component comp = table.getTableHeader().getDefaultRenderer()
                            .getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
                    if (comp instanceof JLabel) {
                        ((JLabel)comp).setHorizontalAlignment(JLabel.CENTER);
                        return comp;
                    } else {
                        JLabel label = (JLabel)cellRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
                        label.setHorizontalAlignment(JLabel.CENTER);
                        label.setBackground(comp.getBackground());
                        label.setForeground(comp.getForeground());
                        label.setFont(comp.getFont());
                        return label;
                    }
                    
                }
            };

            final TableCellRenderer midAlignRender = new TableCellRenderer() {
                private DefaultTableCellRenderer  cellRenderer = new DefaultTableCellRenderer();
                public java.awt.Component getTableCellRendererComponent(JTable table, Object value,
                        boolean isSelected, boolean hasFocus, int row, int column) {
                    JLabel label;
                    java.awt.Component comp = table.getDefaultRenderer(String.class)
                            .getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
                    if (comp instanceof JLabel) {
                        label = ((JLabel)comp);
                    } else {
                        label = (JLabel)cellRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
                        label.setHorizontalAlignment(JLabel.CENTER);
                        label.setForeground(comp.getForeground());
                        label.setFont(comp.getFont());
                        //label.setFont(new Font("Serif", table.getFont().getStyle(), 14));
                    }

                    if (isHeaderRow(row)) {
                        label.setHorizontalAlignment(JLabel.LEFT);
                        label.setBackground(Color.LIGHT_GRAY);
                    } else {
                        label.setHorizontalAlignment(JLabel.CENTER);
                        label.setBackground(Color.WHITE);
                    }

                    return label;
                }
            };

            final TableCellRenderer seqAlignRender = new TableCellRenderer() {
                public java.awt.Component getTableCellRendererComponent(JTable table, Object value,
                        boolean isSelected, boolean hasFocus, int row, int column) {
                   if (isHeaderRow(row)) {
                       return midAlignRender.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
                   }

                   JTextPane textPane = new JTextPane();
                   textPane.setForeground(table.getForeground());
                   textPane.setBackground(table.getBackground());
                   textPane.setFont(new Font("Monospaced", table.getFont().getStyle(), 14));

                   if (value==null) {
                       return textPane;
                   }

                   String str = value.toString();
                   Pattern p = Pattern.compile("\\[(.+)(.)\\]");
                   Matcher m = p.matcher(str);
                   if (m.find()) {
                       int site = m.start();
                       double score = Double.parseDouble(m.group(1));

                       textPane.setText(m.replaceFirst("$2"));
                       
                       StyledDocument doc = textPane.getStyledDocument();
                       SimpleAttributeSet attrSet = new SimpleAttributeSet();
                       Color color = getColorOfSite(score);
                       StyleConstants.setBackground(attrSet, color);
                       StyleConstants.setForeground(attrSet, invertColor(color));
                       doc.setCharacterAttributes(site, 1, attrSet, true);

                       attrSet = new SimpleAttributeSet();
                       StyleConstants.setAlignment(attrSet, StyleConstants.ALIGN_CENTER);
                       doc.setParagraphAttributes(0, 0, attrSet, true);
                   }
                   return textPane;
                }
            };

            TableColumnModel colModel = getColumnModel();

            // header column
            int colSeq = colModel.getColumnIndex(headerSeq);
            TableColumn column = colModel.getColumn(colSeq);
            column.setCellRenderer(seqAlignRender);
            //column.setHeaderRenderer(headAlignRender);

            int nCol = colModel.getColumnCount();
            for (int i=0; i<nCol; i++) {
                if (i!=colSeq) {
                    column = colModel.getColumn(i);
                    column.setCellRenderer(midAlignRender);
                    column.setHeaderRenderer(headAlignRender);
                }
            }
        }

        private void setPreferredColumnWidths(double[] percentages) {
            java.awt.Dimension tableDim = this.getPreferredSize();

            double total = 0;
            for(int i = 0; i < getColumnModel().getColumnCount(); i++)
                total += percentages[i];

            for(int i = 0; i < getColumnModel().getColumnCount(); i++)
            {
                TableColumn column = getColumnModel().getColumn(i);
                column.setPreferredWidth((int) (tableDim.width * (percentages[i] / total)));
            }
       }

        private boolean isHeaderRow(int row) {
            if (row<0||row>=getRowCount())
                return false;

            Object value = getModel().getValueAt(row, 0);
            if (value==null)
                return false;

            return value.toString().startsWith(">");
        }

        public void setResult(final PhosidePredictionResult result) {
            if (result==null) {
                setModel(new DefaultTableModel());
                return;
            }
            
            Vector data = formatData(result,true
                    ,accComboBox.getSelectedIndex()>=accComboBox.getItemCount()-1);

            setModel(new DefaultTableModel(data, header));
            setCellRenderer();
            setPreferredColumnWidths(new double[]{1.0,1.0,4.0,1.0,1.0});
        }
    }

    class ViewResultTextPane extends JTextPane {
        private final int scorePrecision = 5;
        private boolean suppressRepaint = false;

        public ViewResultTextPane() {
            this.setEditable(false);
            setFont(new Font("Monospaced", getFont().getStyle(), 14));
            //this.setForeground(Color.DARK_GRAY);
        }

        public void repaint(long tm, int x, int y, int width, int height) {
            if (!suppressRepaint)
                super.repaint(tm, x, y, width, height);
        }

//        public void paint(java.awt.Graphics g) {
//            if (!suppressRepaint)
//                super.paint(g);
//        }

        public void setResult(final PhosidePredictionResult result) {
            // formart sequence
            StringWriter stringWriter = new StringWriter();
            DefaultFastaProteinsWriter proteinsWriter
                    = new DefaultFastaProteinsWriter(stringWriter);
            proteinsWriter.setDisplayNumber(true);
            proteinsWriter.setAAPerWord(10);

            proteinsWriter.setResidueAnnotator(new ResidueAnnotator() {
                public String annotate(Protein protein, int index) {
                    String acc = protein.getAccession();
                    if (groundTruth==null) {
                        List<Double> prediction = result.getPredictions(acc);
                        String score = String.format("%."+scorePrecision+"f",prediction.get(index));
                        return "["+score+protein.getSequence().charAt(index)+"]";
                    }

                    Set<Integer> trueSites = new TreeSet();
                    Protein pro = groundTruth.getProtein(protein.getAccession());
                    if (pro!=null) {
                        trueSites = PhosphoDataUtil.getSites(pro);
                    }

                    if (trueSites.isEmpty()) {
                        List<Double> prediction = result.getPredictions(acc);
                        String score = String.format("%."+scorePrecision+"f",prediction.get(index));
                        return "["+score+protein.getSequence().charAt(index)+"]";
                    }

                    Set<Integer> predSites = PhosphoDataUtil.getSites(protein);

                    String ret = "[";
                    if (predSites.contains(index)) {
                        List<Double> prediction = result.getPredictions(acc);
                        String score = String.format("%."+scorePrecision+"f",prediction.get(index));
                        ret += score;
                    }

                    if (trueSites.contains(index)) {
                        ret += "p";
                    }

                    ret += protein.getSequence().charAt(index)+"]";
                    
                    return ret;
                }

                public TreeSet<Integer> indicesOfResidues(Protein protein) {
                    TreeSet<Integer> sites = PhosphoDataUtil.getSites(protein);

                    if (groundTruth!=null) {
                        Protein pro = groundTruth.getProtein(protein.getAccession());
                        if (pro!=null) {
                            sites.addAll(PhosphoDataUtil.getSites(pro));
                        }
                    }

                    return sites;
                }
            });

            try {
                proteinsWriter.write(result);
            } catch(java.io.IOException e) {
                e.printStackTrace();
            }

            String str = stringWriter.toString();
            str = str.replaceAll("\r\n", "\n"); // convert to unix style

            // replace sites and return location
            HashMap<Integer,Double> mapLocScore = new HashMap();
            HashSet<Integer> trueSites = new HashSet();
            int nRemoved = 0;
            Pattern p = Pattern.compile("\\[(-?[0-9]*\\.[0-9]{"+scorePrecision+"})?(p?).\\]");
            Matcher m = p.matcher(str);
            while (m.find()) {
                int start = m.start();
                int site = start-nRemoved;

                String s = m.group(1);
                if (s!=null && s.length()>0) {
                    nRemoved += s.length();//[]

                    Double score = Double.valueOf(s);
                    mapLocScore.put(site, score);
                }

                s = m.group(2);
                if (s!=null&&s.length()==1) {
                    nRemoved++; //p
                    trueSites.add(site);
                }

                nRemoved += 2;//[]
            }
            str = str.replaceAll("\\[(-?[0-9]*\\.[0-9]{"+scorePrecision+"})?p?(.)\\]", "$2");

            // header
            List<int[]> headerLocs = new ArrayList();
            p = Pattern.compile(">[^\n]+");
            m = p.matcher(str);
            while (m.find()) {
                int[] locs = new int[2];
                locs[0] = m.start();
                locs[1] = m.end();
                headerLocs.add(locs);
            }

            // line number
            List<int[]> numberLocs = new ArrayList();
            p = Pattern.compile("\n([0-9]+ *)");
            m = p.matcher(str);
            while (m.find()) {
                int[] locs = new int[2];
                locs[0] = m.start(1);
                locs[1] = m.end(1);
                numberLocs.add(locs);
            }

            suppressRepaint = true;

            // set text
            setText(str);

            // set style
            StyledDocument doc = getStyledDocument();

            {
                // render header
                SimpleAttributeSet attrSet = new SimpleAttributeSet();
                StyleConstants.setBackground(attrSet, Color.LIGHT_GRAY);
                for (int[] header : headerLocs) {
                    doc.setCharacterAttributes(header[0], header[1]-header[0], attrSet, false);
                }
            }
            
            {
                // render number
                SimpleAttributeSet attrSet = new SimpleAttributeSet();
                StyleConstants.setForeground(attrSet, Color.GRAY);
                for (int[] num : numberLocs) {
                    doc.setCharacterAttributes(num[0], num[1]-num[0], attrSet, false);
                }
            }

            {
                // render predictions by highlighting
                for (Map.Entry<Integer,Double> entry : mapLocScore.entrySet()) {
                    int site = entry.getKey();
                    double score = entry.getValue();
                    Color color = getColorOfSite(score);
                    SimpleAttributeSet attrSet = new SimpleAttributeSet();
                    StyleConstants.setBackground(attrSet, color);
                    StyleConstants.setForeground(attrSet, invertColor(color));
                    //StyleConstants.setUnderline(attrSet, true);
                    doc.setCharacterAttributes(site, 1, attrSet, false);
                }

                // render true sites by underlining
                for (int site : trueSites) {
                    SimpleAttributeSet attrSet = new SimpleAttributeSet();
                    StyleConstants.setUnderline(attrSet, true);
                    doc.setCharacterAttributes(site, 1, attrSet, false);
                }
            }

            suppressRepaint = false;
            repaint();
        }
    }

    private class NumSitesComboBoxEditor implements javax.swing.ComboBoxEditor {
        private javax.swing.JTextField tfInput;

        public NumSitesComboBoxEditor() {
            tfInput = new javax.swing.JTextField("0");
        }

        public Integer getNum() {
            return Integer.valueOf(tfInput.getText());
        }

        public void setNum(int num) {
            try {
                if (getNum()==num)
                    return;
            } catch(Exception e) {
            }
            
            tfInput.setText(""+num);
        }

        public void addActionListener(java.awt.event.ActionListener l) {
            tfInput.addActionListener(l);
        }

        public java.awt.Component getEditorComponent() {
            return tfInput;
        }

        public Object getItem() {
            return tfInput.getText();
        }

        public void removeActionListener(java.awt.event.ActionListener l) {
            tfInput.removeActionListener(l);
        }

        public void selectAll() {
        }

        public void setItem(Object anObject) {
            if (anObject == null || lockNumSites) {
                    return;
            }

            if (anObject instanceof Integer) {
                setNum((Integer)anObject);
            }
        }
    }
}
